ADRIÁN RUBIO PINTADO

PIT - Práctica 3: Detección de Actividad de Voz (VAD)

Alicia Lozano Díez y Pablo Ramírez Hereza

7 de marzo de 2022

Objetivo

El objetivo de esta práctica es proporcionar una introducción al procesamiento de señales temporales de voz, y desarrollar de un detector de actividad de voz basado en redes neuronales recurrentes, en particular, LSTM.

Materiales

CUIDADO: * Los datos proporcionados son de uso exclusivo para esta práctica. No tiene permiso para copiar, distribuir o utilizar el corpus para ningún otro propósito.

1. Introducción al procesamiento de señales temporales de voz

1.1. Descarga de ficheros de ejemplo

Primero vamos a descargar el audio de ejemplo de Moodle (audio_sample.wav) y ejecutar las siguientes líneas de código, que nos permitirán subir el archivo a Google Colab desde el disco local:

Una vez cargado el fichero de audio, podemos escucharlo de la siguiente manera:

1.2. Lectura y representación de audio en Python

A continuación vamos a definir ciertas funciones para poder hacer manejo de ficheros de audio en Python.

Comenzamos definiendo una función read_recording que leerá un fichero de audio WAV, normalizará la amplitud y devolverá el vector de muestras signal y su frecuencia de muestreo fs.

Si ejecutamos la función anterior para el fichero de ejemplo, podemos ver la forma en la que se carga dicho fichero de audio en Python. Así, podemos obtener la frecuencia de muestreo y la longitud del fichero en número de muestras:

PREGUNTAS:

- ¿Como obtendría la duración de la señal en segundos?

Con la frecuecia de muestreo, cogemos el inverso para ver el tamaño de ventana y lo multiplicamos por el número de muestras.

Obtenemos 4 segundos

También podemos representar la señal y ver su forma de onda. Para ello, definimos la función plot_signal como sigue:

Y utilizando la función anterior, obtenemos su representación (amplitud frente al tiempo):

PREGUNTAS: - Incluya en el informe la representación obtenida.

1.3. Representación de etiquetas de actividad de voz

En esta práctica, vamos a desarrollar un detector de actividad de voz, que determinará qué segmentos de la señal de voz son realmente voz y cuáles silencio.

Por ello, vamos a ver dos ejemplos de etiquetas ground truth, que corresponden al fichero de audio de ejemplo.

Primero, descargamos de Moodle las etiquetas de voz/silencio que están en los ficheros audio_sample_labels_1.voz y audio_sample_labels_2.voz y las cargamos en Google Colab como en el caso anterior.

Estas etiquetas están guardadas en ficheros de texto y podemos cargarlas en Python de la siguiente manera:

Con el siguiente código, podemos representar la señal de voz así como sus etiquetas en la misma figura:

Las etiquetas de voz/silencio provienen de distintos detectores de actividad de voz.

PREGUNTAS: - ¿Qué valores tienen las etiquetas? ¿Qué significan dichos valores? Las etiquetas corresponden a una señal cuadrada que toma el valor 1 cuando detecta voz y -1 cuando detecta silencio en la señal a etiquetar.

- ¿Por qué se representa _voicelabels*2-1? Porque voice_labels es una señal binaria que toma valores entre 0 y 1, la amplitud de voz de nuestra señal normalizada toma valores entre 1 y -1. Haciendo voicelabels*2-1, lo que hacemos es que los valores de las etiquetas que antes estaban en 1 se mantengan en 1, y los que estaban en 0 se mapeen a -1.

- Represente la señal de voz junto con las etiquetas para ambos casos e incluya las figuras en el informe de la práctica. ¿Qué diferencias observas? ¿A qué se puede deber?

PARA audio_sample_labels_1.voz:

PARA audio_sample_labels_2.voz

Vemos que voice_labels_2, es mucho menos agresiva a la hora de considerar silencios. Cuando hay intervalos con un umbral de amplitud un poco más alto(por ejemplo segundo 1.8), considera dichos intervalos como voz y no como silencio. El umbral de amplitud para considerar una ventana como silencio en este sistema es más bajo que en voice_labels_1. Como resultado, voice_labels_2 hace más suaves los cambios temporales entre voz y no voz.

- ¿Qué cantidad de voz/silencio hay en cada etiquetado?

Consideramos la frecuencia de muestreo de la señal de voz y el tiempo que ocupa cada label "voz" o "silencio" dentro de ella:

1.4. Extracción de características

En la mayoría de sistemas de reconocimiento de patrones, un primer paso es la extracción de características. Esto consiste, a grandes rasgos, en obtener una representación de los datos de entrada, que serán utilizados para un posterior modelado.

En nuestro caso, vamos pasar de la señal en crudo "raw" dada por las muestras (signal), a una secuencia de vectores de características que extraigan información a corto plazo de la misma y la representen. Esta sería la entrada a nuestro sistema de detección de voz basado en redes neuronales.

Para ver algunos ejemplos, vamos a utilizar la librería librosa (https://librosa.org/doc/latest/index.html).

Dentro de esta librería, tenemos funciones para extraer distintos tipos de características de la señal de voz, como por ejemplo el espectrograma en escala Mel (melspectrogram).

Estas características a corto plazo, se extraen en ventanas de unos pocos milisegundos con o sin solapamiento.

Un ejemplo sería el siguiente:

PREGUNTAS:

- ¿Qué se obtiene de la función anterior?

El array con el espectrograma Mel.

- ¿Qué significan los valores de los parámetros _winlength y _hoplength?

win_len:tamaño de la ventana, es decir, el número de muestas que cogemos en cada frame

hop_len: El desplazamiento que cojo dentro del tamaño de ventana sobre el que empiezo a solapar las ventanas cogidas de las muestras. En nuestro caso tenemos un win_length=320 y un hop_length=160, con lo que estamos solapando sobre la mitad cada ventana.

- ¿Qué dimensiones de _melspec obtienes? ¿Qué significan? Obtenemos un shape de(23, 420), es decir, 23 vectores de características de tamaño 420.


De esta manera, podríamos obtener una parametrización de las señales para ser utilizadas como entrada a nuestra red neuronal.

Para los siguientes apartados, se proporcionan los vectores de características MFCC para una serie de audios que se utilizarán como conjunto de entrenamiento del modelo de VAD.

2. Detector de actividad de voz (Voice Activity Detector, VAD)

2.1. Descarga de los datos de entrenamiento

Primero vamos a descargar la lista de identificadores de los datos de entrenamiento de la práctica.

Para ello, necesitaremos descargar de Moodle el fichero training_VAD.lst, y ejecutar las siguientes líneas de código, que nos permitirán cargar el archivo a Google Colab desde el disco local:

A continuación cargamos los identificadores contenidos en el fichero en una lista en Python:

Podemos ver algunos de ellos (los primeros 10 identificatores) de la siguiente forma:

Ahora, descargaremos de Moodle el fichero data_download_onedrive_training_VAD.sh, y ejecutaremos las siguientes líneas de código, que nos permitirán cargar el archivo a Google Colab desde el disco local:

Para descargar el conjunto de datos desde One drive, ejecutamos el script cargado anteriormente de la siguiente manera:

Este script descargará los datos de One Drive y los cargará en Google Colab, descomprimiéndolos en la carpeta data/training_VAD.

Podemos comprobar que los ficheros .mat se encuentran en el directorio esperado:

2.2. Definición del modelo

Utilizando la librería Pytorch (https://pytorch.org/docs/stable/index.html), vamos a definir un modelo de ejemplo con una capa LSTM y una capa de salida. La capa de salida estará formada por una única neurona. La salida indicará la probabilidad de voz/silencio utilizando una función sigmoid.

PREGUNTAS: - ¿Qué tamaño tiene la entrada a la capa LSTM? El tamaño del vector de características, feat_dim.

- ¿Cuántas unidades (celdas) tiene dicha capa LSTM? 256

- ¿Qué tipo de matriz espera la LSTM? Mirar la documentación y describir brevemente.

Los datos de entrada son tensores(matrices). Espera dos entradas, concretamente input y (h_0, c_0).

Con input: es un tensor con las características de entrada de la secuencia. Tiene dimensiones tensor of shape (Longitud secuencia ,Tamaño de la entrada) para una entrada unbatched, (Longitud secuencia , Tamaño del Batch, Tamaño de la entrada) cuando batch_first=False, y ( Tamaño del Batch, Longitud secuencia , Tamaño de la entrada) cuando cuando batch_first=True.

h_0: Tensor con los estados iniciales ocultos para cada elemento en el batch. Por defecto ceros si no se le pasa como argumento.

c_0: Tensor con los estados iniciales de cada celda para cada elemento en el batch.Por defecto ceros si no se le pasa como argumento.

- Revisar la documentación de torch.nn.LSTM y describir brevemente los argumentos _batchfirst, bidirectional y dropout.

batch_first: Si está a True, los tensores de entrada y salida son proporcionados como (batch, seq, feature) en vez de (seq, batch, feature). Por defecto está a False. Es decir, cambia el orden de llamada.

bidirectional: Si está a True, la LTSM se convierte en bidireccional.

dropout: Si no es cero, se añade una capa de dropout en las salidas de cada capa LSTM exceptuando la última, con valor de probabilidad el valor pasado como dropout. Es decir, ignorar a veces algunas celdas para evitar la dependencia total de la red sobre ellas.

- En este modelo, estamos utilizando una única neurona a la salida. ¿Hay alguna otra alternativa? ¿Se seguiría utilizando una función sigmoid?

Sí, podríamos utiliar una función softmax, con dos neuronas de salida, donde cada neurona representa la probabilidad de que la salida sea voz o silencio.

- ¿Para qué sirve la función forward definida en la clase _Model1? Sirve para conectar la arquitectura de la red que hemos creado, la LSTM con la salida(sigmoide) para definir nuestro modelo, proporcionando la salida en el formato deseado.

Una vez definida la clase, podemos crear nuestra instancia del modelo y cargarlo en la GPU con el siguiente código:

Nuestra variable model contiene el modelo, y ya estamos listos para entrenarlo y evaluarlo.

2.3. Lectura y preparación de los datos para el entrenamiento

Como hemos visto anteriormente, nuestros datos están guardados en ficheros de Matlab (.mat). Cada uno de estos ficheros contiene una matriz X correspondiente a las secuencias de características MFCC (con sus derivadas de primer y segundo orden), y un vector Y con las etiquetas de voz/silencio correspondientes.

Veamos un ejemplo:

PREGUNTAS: Elegir un fichero de entrenamiento y responder a las siguientes preguntas:

Features (46654, 60) Y labels(46654, 1).

Con el número de ventanas o frames tomados.

El entrenamiento del modelo se va a realizar mediante descenso por gradiente (o alguna de sus variantes) basado en batches.

Para preparar cada uno de estos batches que servirán de entrada a nuestro modelo LSTM, debemos almacenar las características en secuencias de la misma longitud. El siguiente código lee las características (get_fea) y sus correspondientes etiquetas (get_lab) de un fragmento aleatorio del fichero de entrada.

PREGUNTAS: Analizar las funciones anteriores detenidamente y responder a las siguientes cuestiones:

2.4. Entrenamiento del modelo

Una vez definidas las funciones de lectura de datos y preparación del formato que necesitamos para la entrada a la red LSTM, podemos utilizar el siguiente código para entrenarlo.

PREGUNTAS: Analizar el código anterior cuidadosamente y ejecutarlo. A continuación, responder a las siguientes cuestiones:

- ¿Qué función de coste se está optimizando? Describir brevemente con ayuda de la documentación. Se utiliza BCELoss, que mide la entropía cruzada binaria( Binary Cross Entropy) entre el objetivo y las probabilidades de entrada tal que:

$$l_{n} = -w_{n}[y_{n}logx_{n} + (1-y_{n})log(1-x_{n})]$$

Siendo x cada batch. Después se hace la metdia de $l_{n}$ para cada batch(o la suma si reduction=None)

- ¿Qué optimizador se ha definido?

El optimizador Adam con una tasa de aprendizaje de lr=0.001. Es un algoritmo para la optimización basada en el gradiente de primer orden de funciones objetivo estocásticas, basado en estimaciones adaptativas de momentos de orden inferior.

- ¿Para qué se utiliza _batchsize?

Para decirle al cargador de datos cuantos datos de entrada tiene que coger por lotes para pasarselos a la red en cada iteración en vez de hacerlo de uno en uno.

- Describir brevemente la creación de los batches.

Cogemos indices aleatorios para coger filas de los datos de entrenamiento(para las etiquetas en concordancia a los features de entrenamiento, esto mediante las funciones get_fea() y get_lab() ). Los apilamos en arrays numpy verticalmente con np.vstack. Los permutamos y los almacenamos en formato de tensor.

Este proceso se repite para cada época.

- ¿Qué línea de código realiza el forward pass?

outputs = model(train_batch)


- ¿Qué línea de código realiza el backward pass?

loss.backward() Que calcula los deltas(errores) mediante backpropagation, de ahí que llame al objeto de loss. Luego

optimizer.step() El optimizador actualiza los pesos con los gradientes que ha calculado.

- ¿Cuántas iteraciones del algoritmo ha realizado? ¿Qué observa en la evolución de la función de coste?

4 épocas, que su valor va disminuyendo en cada iteración, acercándose cada vez más a 0.

- Añada al código el cálculo de la precisión o accuracy, de tal manera que se muestre por pantalla dicho valor en cada iteración (similar a lo que ocurre con el valor del coste loss). Copiar el código en el informe y describir brevemente.

El codigo se incorpora en el código de arriba. Calculamos el accuracy contando el número de veces que nuestro podemos acierta dividido entre el numero total de casos a comprobar. Es decir, para cada predicción vemos si el resultado predicho es el del label. Cabe tener en cuenta que la salida de la red es una probabilidad, por ello si es mayor o igual de 0.5 se considera clase 1, si no clase 0.

- ¿Qué valor de coste y accuracy obtiene? ¿Cómo se puede mejorar?

Para la última época un valos de perdida de 0.053 y un Accuracy de 0.895.

Se podría mejorar empleando más épocas en el entranamiento y utilizando si estuvieran disponibles un conjunto de datos de entrenamiento mayor.

2.5. Evaluación del modelo: un único fichero de test

Una vez entrenado el modelo, vamos a evaluarlo en un ejemplo en concreto.

Descargue de Moodle el fichero audio_sample_test.wav, con sus correspondientes características y etiquetas audio_sample_test.mat y evalúe el rendimiento en el mismo.

PREGUNTAS:

Un accuracy del 90,73%

ETIQUETAS GROUND TRUTH:

ETIQUETAS PREDICHAS POR NUESTRO MODELO:

Podemos afirmar que visualmente nuestro modelo es bueno

Escuchando los 10 primeros segundos vemos como solo aparece una voz al segundo 6, tal y como detecta noestro modelo. Asi que podemos decir, tras ver el acuraccy del 90% anterior, que nuestro modelo es bueno.

2.6. Evaluación del modelo: conjunto de validación

Ahora vamos a evaluar el rendimiento del modelo anterior sobre un conjunto de validación (del que conocemos sus etiquetas).

Para este conjunto de datos, descargaremos la lista de identificadores valid_VAD.lst de Moodle, así como el fichero de descarga de datos data_download_onedrive_valid_VAD.sh:

Escriba ahora el código necesario para evaluar el modelo anterior en el conjunto de datos de validación, para su última época.

Tenga en cuenta que si quiere realizar el forward para todos los datos de validación de una vez, necesitará que todas las secuencias sean de la misma longitud. Como aproximación, puede escoger unos pocos segundos de cada fichero como se hace en el entrenamiento.

CODIGO PARA LA VALIDACIÓN:

PREGUNTAS:

Realizamos otro número máximo de 4 épocas. Es decir, al Modelo_1 ya entrenado, le entrenamos otras 4 épocas mediante el conjunto de validación. En principio no cambiamos más hiperparámetros.

Tras el entrenamiento, obteníamos unos valores de: loss = 0.05 accuracy = 0.89

Tras la validación, vemos como mejoramos las métricas hasta la época 4+3 = 7, pero en la octava época bajamos los valores de las métricas, indicándonos que estamos haciendo a partir de ahí overfitting de los datos.

Época 7(Mejora máxima ampliando nº de épocas):

loss = 0.003 accuracy = 0.902

Época 8(Overfitting):

loss = 0.004 accuracy = 0.862

3. Comparación de modelos

3.1. Redes LSTM bidireccionales

En este apartado, vamos a partir del modelo inicial (_Model1) y modificarlo para que la capa LSTM sea bidireccional (_Model1B).

Entrene el nuevo modelo y compare el resultado con el modelo inicial.

ENTRENAMOS EL NUEVO MODELO MODELO_1B

Para la época final del train(época 4 obtenemos):

loss = 0.046 accuracy = 0.907

VALIDAMOS EL NUEVO MODELO MODELO_1B

PREGUNTAS:

Una capa LSTM solo aprenden de eventos “pasados” en el tiempo, mientras que una capa bidireccional incorpor información de pasado y de futuro. Es decir, las bidireccionales tienen dos capas, una que va hacia delante en el tiempo recorriendo los datos, y una que va hacia atras en el tiempo.

El código se encuentra encima de estas preguntas.

Comparando en ambos la época 7(3 de validación), ya que es la última previa al overfitting, tenemos

MODELO_1:Época 7:

loss = 0.003
accuracy = 0.902

MODELO_1B:Época 7:

Loss: 0.002
Accuracy: 0.915

Es decir, obtenemos unos resultados algo mejores en el modelo con la LSTM bidireccional. Esto se puede deber a que este último modelo acaba teniendo más parámetros a ajustar durante el entrenamiento, debido a que la nuevo capa que va atrás en el tiempo, añade más parámetros al modelo total, y luego para predecir, se tienen en cuenta las 2 capas, que contrastan.

3.2. Modelo "más profundo"

En este apartado, vamos a partir nuevamente del modelo _Model1 y vamos a añadir una segunda capa LSTM tras la primera, con el mismo tamaño y configuración, definiendo un nuevo modelo _Model2.

Entrénelo y compare los resultados.

ENTRENAMOS EL NUEVO MODELO MODELO_2

VALIDAMOS EL NUEVO MODELO MODELO_2

PREGUNTAS:

El código se encuentra justo encima de estas cuestiones.

Para el modelo 2 vecmos como la mejor puntuación la encontramos en la época 2 de validación(ya que a partir de alli empiza la fluctuación y el overfitting).

MODELO_2:Época 2:

loss =  0.002453913705216514
accuracy= 0.9323611111111111

Por lo tanto, la mejor marca la obtiene el modelo 2. Esto se puede deber a que tiene más parámetros a ajustar durante el entrenamiento, y obteniene una mayor precisión. Dado que dispone de 2 LSTM's dispone de una "mayor memoria".

En este caso el modelo 2 de nuevo, es el que obtiene una mejor puntuación. Habíamos visto tanto para el modelo_1b como para el modelo_2, que mejoraban la puntuación del modelo_1, ya que ambos tenían un mayor número de parámtros al final, y por tanto eran más precisos.

Sin embargo, hay diferentes maneras de añadir más parámetros(1 capa bidireccional o 2 capas apiladas.) Dada la naturaleza de este problema de deteccion de voz/silencio, vemos como obtenemos mejores resultados con 2 LSTM's apiladas, que consiguen tener una "memoria mayor" que una sola capa LSTM(modelo_1), y mayor relevancia que disponer de valores futuros con una LSTM bidireccional(modelo_2).